/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.gallery3d.app;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import android.os.Handler;
import android.os.Message;
import android.os.Process;

import com.android.gallery3d.common.Utils;
import com.android.gallery3d.data.ContentListener;
import com.android.gallery3d.data.MediaItem;
import com.android.gallery3d.data.MediaObject;
import com.android.gallery3d.data.MediaSet;
import com.android.gallery3d.data.Path;
import com.android.gallery3d.ui.SynchronizedHandler;

public class AlbumDataLoader {
	private static final String TAG = AlbumDataLoader.class.getSimpleName();
	private static final int DATA_CACHE_SIZE = 1000;

	private static final int MSG_LOAD_START = 1;
	private static final int MSG_LOAD_FINISH = 2;
	private static final int MSG_RUN_OBJECT = 3;

	private static final int MIN_LOAD_COUNT = 32;
	private static final int MAX_LOAD_COUNT = 64;

	private final MediaItem[] mData;
	private final long[] mItemVersion;
	private final long[] mSetVersion;

	public static interface DataListener {
		public void onContentChanged(int index);

		public void onSizeChanged(int size);
	}

	private int mActiveStart = 0;
	private int mActiveEnd = 0;

	private int mContentStart = 0;
	private int mContentEnd = 0;

	private final MediaSet mSource;
	private long mSourceVersion = MediaObject.INVALID_DATA_VERSION;

	private final Handler mMainHandler;
	private int mSize = 0;

	private DataListener mDataListener;
	private MySourceListener mSourceListener = new MySourceListener();
	private LoadingListener mLoadingListener;

	private ReloadTask mReloadTask;
	// the data version on which last loading failed
	private long mFailedVersion = MediaObject.INVALID_DATA_VERSION;

	public AlbumDataLoader(AbstractGalleryActivity context, MediaSet mediaSet) {
		mSource = mediaSet;

		mData = new MediaItem[DATA_CACHE_SIZE];
		mItemVersion = new long[DATA_CACHE_SIZE];
		mSetVersion = new long[DATA_CACHE_SIZE];
		Arrays.fill(mItemVersion, MediaObject.INVALID_DATA_VERSION);
		Arrays.fill(mSetVersion, MediaObject.INVALID_DATA_VERSION);

		mMainHandler = new SynchronizedHandler(context.getGLRoot()) {
			@Override
			public void handleMessage(Message message) {
				switch (message.what) {
				case MSG_RUN_OBJECT:
					Log.d(TAG, "MainHandler -- RUN OBJECT");
					((Runnable) message.obj).run();
					return;
				case MSG_LOAD_START:
					Log.d(TAG, "MainHandler -- LOAD START");
					if (mLoadingListener != null)
						mLoadingListener.onLoadingStarted();
					return;
				case MSG_LOAD_FINISH:
					Log.d(TAG, "MainHandler -- RUN FINISH");
					if (mLoadingListener != null) {
						boolean loadingFailed = (mFailedVersion != MediaObject.INVALID_DATA_VERSION);
						mLoadingListener.onLoadingFinished(loadingFailed);
					}
					return;
				}
			}
		};
	}

	public void resume() {
		Log.d(TAG, "resume");
		mSource.addContentListener(mSourceListener);
		mReloadTask = new ReloadTask();
		mReloadTask.start();
	}

	public void pause() {
		Log.d(TAG, "pause");
		mReloadTask.terminate();
		mReloadTask = null;
		mSource.removeContentListener(mSourceListener);
	}

	public MediaItem get(int index) {
		Log.d(TAG, "get -- index : " + index);
		if (!isActive(index)) {
			return mSource.getMediaItem(index, 1).get(0);
		}
		return mData[index % mData.length];
	}

	public int getActiveStart() {
		return mActiveStart;
	}

	public boolean isActive(int index) {
		return index >= mActiveStart && index < mActiveEnd;
	}

	public int size() {
		return mSize;
	}

	// Returns the index of the MediaItem with the given path or
	// -1 if the path is not cached
	public int findItem(Path id) {
		for (int i = mContentStart; i < mContentEnd; i++) {
			MediaItem item = mData[i % DATA_CACHE_SIZE];
			if (item != null && id == item.getPath()) {
				return i;
			}
		}
		return -1;
	}

	private void clearSlot(int slotIndex) {
		Log.d(TAG, "clearSlot -- index : " + slotIndex);
		mData[slotIndex] = null;
		mItemVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
		mSetVersion[slotIndex] = MediaObject.INVALID_DATA_VERSION;
	}

	private void setContentWindow(int contentStart, int contentEnd) {
		Log.d(TAG, "setContentWindow -- contentStart : " + contentStart
				+ " contentEnd : " + contentEnd);
		if (contentStart == mContentStart && contentEnd == mContentEnd) {
			return;
		}
		int end = mContentEnd;
		int start = mContentStart;

		// We need change the content window before calling reloadData(...)
		synchronized (this) {
			mContentStart = contentStart;
			mContentEnd = contentEnd;
		}
		// long[] itemVersion = mItemVersion;
		// long[] setVersion = mSetVersion;
		if (contentStart >= end || start >= contentEnd) {
			for (int i = start, n = end; i < n; ++i) {
				clearSlot(i % DATA_CACHE_SIZE);
			}
		} else {
			for (int i = start; i < contentStart; ++i) {
				clearSlot(i % DATA_CACHE_SIZE);
			}
			for (int i = contentEnd, n = end; i < n; ++i) {
				clearSlot(i % DATA_CACHE_SIZE);
			}
		}
		if (mReloadTask != null)
			mReloadTask.notifyDirty();
	}

	public void setActiveWindow(int start, int end) {
		Log.d(TAG, "setActiveWindow -- start : " + start + " End : " + end);
		if (start == mActiveStart && end == mActiveEnd)
			return;

		Utils.assertTrue(start <= end && end - start <= mData.length
				&& end <= mSize);

		int length = mData.length;
		mActiveStart = start;
		mActiveEnd = end;

		// If no data is visible, keep the cache content
		if (start == end)
			return;

		int contentStart = Utils.clamp((start + end) / 2 - length / 2, 0,
				Math.max(0, mSize - length));
		int contentEnd = Math.min(contentStart + length, mSize);
		if (mContentStart > start || mContentEnd < end
				|| Math.abs(contentStart - mContentStart) > MIN_LOAD_COUNT) {
			setContentWindow(contentStart, contentEnd);
		}
	}

	private class MySourceListener implements ContentListener {
		@Override
		public void onContentDirty() {
			if (mReloadTask != null)
				mReloadTask.notifyDirty();
		}
	}

	public void setDataListener(DataListener listener) {
		mDataListener = listener;
	}

	public void setLoadingListener(LoadingListener listener) {
		mLoadingListener = listener;
	}

	private <T> T executeAndWait(Callable<T> callable) {
		FutureTask<T> task = new FutureTask<T>(callable);
		mMainHandler.sendMessage(mMainHandler.obtainMessage(MSG_RUN_OBJECT,
				task));
		try {
			return task.get();
		} catch (InterruptedException e) {
			return null;
		} catch (ExecutionException e) {
			throw new RuntimeException(e);
		}
	}

	private static class UpdateInfo {
		public long version;
		public int reloadStart;
		public int reloadCount;

		public int size;
		public ArrayList<MediaItem> items;
	}

	private class GetUpdateInfo implements Callable<UpdateInfo> {
		private final long mVersion;

		public GetUpdateInfo(long version) {
			mVersion = version;
		}

		@Override
		public UpdateInfo call() throws Exception {
			if (mFailedVersion == mVersion) {
				// previous loading failed, return null to pause loading
				return null;
			}
			UpdateInfo info = new UpdateInfo();
			long version = mVersion;
			info.version = mSourceVersion;
			info.size = mSize;
			long setVersion[] = mSetVersion;
			for (int i = mContentStart, n = mContentEnd; i < n; ++i) {
				int index = i % DATA_CACHE_SIZE;
				if (setVersion[index] != version) {
					info.reloadStart = i;
					info.reloadCount = Math.min(MAX_LOAD_COUNT, n - i);
					return info;
				}
			}
			return mSourceVersion == mVersion ? null : info;
		}
	}

	private class UpdateContent implements Callable<Void> {

		private UpdateInfo mUpdateInfo;

		public UpdateContent(UpdateInfo info) {
			mUpdateInfo = info;
		}

		@Override
		public Void call() throws Exception {
			UpdateInfo info = mUpdateInfo;
			mSourceVersion = info.version;
			if (mSize != info.size) {
				mSize = info.size;
				if (mDataListener != null) {
					mDataListener.onSizeChanged(mSize);
				}
				if (mContentEnd > mSize) {
					mContentEnd = mSize;
				}
				if (mActiveEnd > mSize) {
					mActiveEnd = mSize;
				}
			}

			ArrayList<MediaItem> items = info.items;

			mFailedVersion = MediaObject.INVALID_DATA_VERSION;
			if ((items == null) || items.isEmpty()) {
				if (info.reloadCount > 0) {
					mFailedVersion = info.version;
					Log.d(TAG, "loading failed: " + mFailedVersion);
				}
				return null;
			}
			int start = Math.max(info.reloadStart, mContentStart);
			int end = Math.min(info.reloadStart + items.size(), mContentEnd);

			for (int i = start; i < end; ++i) {
				int index = i % DATA_CACHE_SIZE;
				mSetVersion[index] = info.version;
				MediaItem updateItem = items.get(i - info.reloadStart);
				long itemVersion = updateItem.getDataVersion();
				if (mItemVersion[index] != itemVersion) {
					mItemVersion[index] = itemVersion;
					mData[index] = updateItem;
					if (mDataListener != null && i >= mActiveStart
							&& i < mActiveEnd) {
						mDataListener.onContentChanged(i);
					}
				}
			}
			return null;
		}
	}

	/**
	 * The thread model of ReloadTask * [Reload Task] [Main Thread] | |
	 * getUpdateInfo() --> | (synchronous call) (wait) <---- getUpdateInfo() | |
	 * Load Data | | | updateContent() --> | (synchronous call) (wait)
	 * updateContent() | | | |
	 */
	private class ReloadTask extends Thread {

		private volatile boolean mActive = true;
		private volatile boolean mDirty = true;
		private boolean mIsLoading = false;

		private void updateLoading(boolean loading) {
			if (mIsLoading == loading) {
				return;
			}
			mIsLoading = loading;
			mMainHandler.sendEmptyMessage(loading ? MSG_LOAD_START
					: MSG_LOAD_FINISH);
		}

		@Override
		public void run() {
			Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

			boolean updateComplete = false;
			while (mActive) {
				synchronized (this) {
					if (mActive && !mDirty && updateComplete) {
						updateLoading(false);
						if (mFailedVersion != MediaObject.INVALID_DATA_VERSION) {
							Log.d(TAG, "reload pause");
						}
						Utils.waitWithoutInterrupt(this);
						if (mActive
								&& (mFailedVersion != MediaObject.INVALID_DATA_VERSION)) {
							Log.d(TAG, "reload resume");
						}
						continue;
					}
					mDirty = false;
				}
				updateLoading(true);
				long version = mSource.reload();
				UpdateInfo info = executeAndWait(new GetUpdateInfo(version));
				updateComplete = info == null;
				if (updateComplete) {
					continue;
				}
				if (info.version != version) {
					info.size = mSource.getMediaItemCount();
					info.version = version;
				}
				if (info.reloadCount > 0) {
					info.items = mSource.getMediaItem(info.reloadStart,
							info.reloadCount);
				}
				executeAndWait(new UpdateContent(info));
			}
			updateLoading(false);
		}

		public synchronized void notifyDirty() {
			mDirty = true;
			notifyAll();
		}

		public synchronized void terminate() {
			mActive = false;
			notifyAll();
		}
	}
}
